package com.xgxx.hy.library.admin.competitionCode.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUnit;
import com.xgxx.hy.library.admin.common.core.domain.AjaxResult;
import com.xgxx.hy.library.admin.common.utils.DateUtils;
import com.xgxx.hy.library.admin.common.utils.date.DateUtil;
import com.xgxx.hy.library.admin.competitionCode.domain.AlgOrder;
import com.xgxx.hy.library.admin.competitionCode.domain.AlgProductionLineMaterial;
import com.xgxx.hy.library.admin.competitionCode.domain.ApsResult;
import com.xgxx.hy.library.admin.competitionCode.domain.ProductionLine;
import com.xgxx.hy.library.admin.competitionCode.service.IAlgOrderService;
import com.xgxx.hy.library.admin.competitionCode.service.IAlgProductionLineMaterialService;
import com.xgxx.hy.library.admin.competitionCode.service.IApsService;
import com.xgxx.hy.library.admin.competitionCode.service.IProductionLineService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;


/**
 * @author zyc
 * @date 2022/10/18
 **/
@Service
public class ApsServiceImpl implements IApsService {

    @Autowired
    private IProductionLineService productionLineService;
    @Autowired
    private IAlgOrderService orderService;
    @Autowired
    private IAlgProductionLineMaterialService configService;
    private List<AlgOrder> orderList;
    private Date startTime;
    private Date endTime;
    private Map<String, List<AlgProductionLineMaterial>> productionLineConfigMap;

    private List<ProductionLine> selectProductLine() {
        List<ProductionLine> productionLineList = new ArrayList<>();
        ProductionLine productionLineA = new ProductionLine();
        productionLineA.setCode("一");
        productionLineA.setName("产线一");
        productionLineA.setTime(new BigDecimal("24"));
        ProductionLine productionLineB = new ProductionLine();
        productionLineB.setCode("二");
        productionLineB.setName("产线二");
        productionLineB.setTime(new BigDecimal("24"));
        ProductionLine productionLineC = new ProductionLine();
        productionLineC.setCode("三");
        productionLineC.setName("产线三");
        productionLineC.setTime(new BigDecimal("24"));
        productionLineList.add(productionLineA);
        productionLineList.add(productionLineB);
        productionLineList.add(productionLineC);
        // return productionLineList;
        return productionLineService.selectBookList(null);
    }

    private List<AlgOrder> selectAlgOrder() {
        /*List<AlgOrder> orderList = new ArrayList<>();
        orderList.add(new AlgOrder("P0001", "甲", "A", "1000", "1"));
        orderList.add(new AlgOrder("P0002", "甲", "B", "2000", "1"));
        orderList.add(new AlgOrder("P0003", "甲", "C", "1000", "1"));
        orderList.add(new AlgOrder("P0004", "甲", "D", "2000", "1"));
        orderList.add(new AlgOrder("P0005", "乙", "A", "2000", "1"));
        orderList.add(new AlgOrder("P0006", "乙", "B", "3000", "1"));
        orderList.add(new AlgOrder("P0007", "丙", "A", "2500", "1"));
        orderList.add(new AlgOrder("P0008", "丙", "B", "2000", "1"));
        orderList.add(new AlgOrder("P0009", "丁", "A", "1200", "1"));
        orderList.add(new AlgOrder("P0010", "丁", "B", "1400", "1"));*/
        // return orderList;
        List<AlgOrder> orderList = orderService.selectAlgOrderList(null);
        return orderList.stream().filter(e -> null != e.getRate()).collect(Collectors.toList());
    }

    private List<AlgProductionLineMaterial> selectAlgProductionLineMaterial() {
        List<AlgProductionLineMaterial> algProductionLineMaterialList = new ArrayList<>();
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("一", "A", "200", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("一", "B", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("一", "C", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("二", "A", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("二", "B", "200", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("二", "D", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("三", "A", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("三", "B", "100", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("三", "C", "200", "1"));
        algProductionLineMaterialList.add(new AlgProductionLineMaterial("三", "D", "200", "1"));
        // return algProductionLineMaterialList;
        return configService.selectAlgProductionLineMaterialList(null);
    }

    public static void main(String[] args) {
        ApsServiceImpl service = new ApsServiceImpl();
        List<AlgOrder> orderList = service.selectAlgOrder();
        service.doAps(orderList, DateUtil.parse("2022-10-09 00:00:00", DateUtil.PATTERN_DATETIME), DateUtil.parse("2022-10-11 00:00:00", DateUtil.PATTERN_DATETIME));
    }

    @Override
    public AjaxResult doAps(List<AlgOrder> orderList, Date startTime, Date endTime) {
        // 缓存数据
        orderList = this.selectAlgOrder();
        this.orderList = orderList;
        this.orderList.forEach(order -> order.setApsQty(order.getQty()));
        this.startTime = startTime;
        this.endTime = endTime;
        this.productionLineConfigMap = this.selectAlgProductionLineMaterial().stream().collect(Collectors.groupingBy(AlgProductionLineMaterial::getProductionLineCode));
        if (startTime.compareTo(endTime) >= 0) {
            return AjaxResult.error("排程结束时间必需大于开始时间！");
        }
        if (CollectionUtil.isEmpty(orderList)) {
            return AjaxResult.error("排程订单不能为空！");
        }
        return this.execute();

    }

    /**
     * 动态规划算法
     * 1、状态要素
     * 当前生产的订单生产结束，并且出现评分比当前物料高的订单时，进行换料
     * 所以有换料、不换料两种状态，使用二维数组r[]比喻结果，r[i]表示最优解，r[i]最优解
     * 2、初始值要素
     * 各个产线的产量*权重来进行评分，优先选择生产时间更长的，使用maxScore1()表示获取到最高评分订单，那么初始值为：
     * r[0] = maxScore1()
     * 3、公式要素
     * 当前阶段不换料，maxScore1()获有两种可能：
     * （1）订单没完成，继续生产，此时只需将结果时间延长一小时
     * （2）订单已完成，但是继续生产其余订单的此物料收益依旧最高，新订单添加至结果
     * 公式可以表达如下：r[i+1] = r[i] + maxScore1()
     * 当前阶段换料，maxScore2()表示是上一个订单持续时间+换料的时间内获取的最高评分，n=上一个订单持续时间+换料的时间
     * 由于我们已经证明了不换料的公式，所以maxScore2()中的上个订单持续时间一定是换料的
     * 所以此阶段换料有两种可能：
     * （1）依旧是上个订单评分最高
     * （2）出现了新订单评分最高
     * 二者取其最高
     * 公式可以表达如下：r[i+1] = max(r[i-n-1]+maxScore2(),r[i])
     * 4、边界要素
     * 边界为endTime或订单排程完成
     * 5、结果
     * 最终取r[i]
     *
     * @return
     */
    private AjaxResult execute() {
        Map<String, List<ApsResult>> map = new HashMap<>();
        Long hours = DateUtil.between(startTime, endTime, DateUnit.HOUR);
        int count = hours.intValue();
        List<ProductionLine> productionLineList = this.selectProductLine();
        Map<String, Map<Integer, List<ApsResult>>> result = new HashMap<>(productionLineList.size());
        productionLineList.forEach(e -> {
            // 状态要素 map第一层表示时间，第二层map表示状态 0不换料 1换料
            Map<Integer, List<ApsResult>> mapTime = new HashMap<>(count);
            result.put(e.getCode(), mapTime);
        });
        // 初始值要素
        this.init(result, productionLineList);
        for (int i = 1; i < count; i++) {
            this.apsByTime(i, result, productionLineList);
        }
        // 处理换料数据
        result.forEach((k, v) -> v.forEach((kt, vt) -> {
            v.put(kt, vt.stream().filter(e -> !e.isTurnFlag()).peek(e -> {
                e.setStartTimeStr(DateUtil.format(e.getStartTime(), DateUtil.PATTERN_DATETIME));
                e.setEndTimeStr(DateUtil.format(e.getEndTime(), DateUtil.PATTERN_DATETIME));
            }).collect(Collectors.toList()));
        }));
        // 结果
        result.forEach((k, v) -> {
            List<List<ApsResult>> list = new ArrayList<>(v.values());
            map.put(k, list.get(list.size() - 1));
        });
        return AjaxResult.success(map);
    }

    private void init(Map<String, Map<Integer, List<ApsResult>>> result, List<ProductionLine> productionLineList) {
        // maxScore1方法有产线竞争规则，当产线同时进入换料阶段时，进入竞争模式
        // 初始化全部进入竞争
        List<ApsResult> list = this.maxScore1(productionLineList);
        list.forEach(e -> {
            // 起始时间
            e.setStartTime(startTime);
            e.setEndTime(startTime);
            Map<Integer, List<ApsResult>> mapTime = result.get(e.getProductionLineCode());
            List<ApsResult> listProductionLine = new ArrayList<>();
            listProductionLine.add(e);
            mapTime.put(0, listProductionLine);
            result.put(e.getProductionLineCode(), mapTime);
            // 订单扣减
            this.subtractOrder(e, 1);
        });
    }

    private void apsByTime(int index, Map<String, Map<Integer, List<ApsResult>>> result, List<ProductionLine> productionLineList) {
        // 待竞争产线，上一小时的订单生产完进入竞争产线
        List<ProductionLine> competeLine = new ArrayList<>();
        productionLineList.forEach(productionLine -> {
            Map<Integer, List<ApsResult>> map = result.get(productionLine.getCode());
            // 查询上一个小时的结果
            List<ApsResult> lastList = map.get(index - 1);
            if (lastList != null) {
                ApsResult lastResult = lastList.get(lastList.size() - 1);
                if (lastResult.isTurnFlag()) {
                    // 上一阶段换料
                    // 获取目标换料
                    ApsResult targetResult = this.maxScore2(productionLine.getCode(), 1);
                    assert null != targetResult;
                    List<AlgProductionLineMaterial> configList = productionLineConfigMap.get(productionLine.getCode());
                    // 换料是否结束
                    AlgProductionLineMaterial config = configList.stream().filter(e -> e.getMaterialCode().equals(targetResult.getMaterialCode())).findFirst().orElse(null);
                    Long turnHours = DateUtil.between(lastResult.getStartTime(), lastResult.getEndTime(), DateUnit.HOUR) + 1;
                    if (config.getTime().compareTo(new BigDecimal(turnHours.intValue())) > 0) {
                        // 获取换料前的结果
                        ApsResult turnLastResult = lastList.get(lastList.size() - 2);
                        // 未换完需要进入重新计算逻辑
                        this.apsRecount(productionLine.getCode(), turnLastResult, lastResult, turnHours.intValue(), index, result);
                    } else {
                        this.apsNewTurn(lastResult, targetResult, index, result);
                        // 订单可生产数减去产量
                        this.subtractOrder(targetResult, 1);
                    }
                } else {
                    // 获取对应的生产订单
                    AlgOrder order = orderList.stream().filter(e -> e.getCode().equals(lastResult.getOrderCode())).findFirst().orElse(null);
                    assert null != order;
                    if (this.checkComplete(order)) {
                        // 不换料未生产完，结果延长一小时
                        this.apsNoTurnNoComplete(lastResult, index, result);
                        // 未完成，订单可生产数减去产量
                        this.subtractOrder(lastResult, 1);
                    } else {
                        // 已完成，进入竞争产线
                        competeLine.add(productionLine);
                    }
                }
            }
        });
        if (competeLine.size() > 0) {
            List<ApsResult> list = this.maxScore1(competeLine);
            list.forEach(apsResult -> {
                Map<Integer, List<ApsResult>> map = result.get(apsResult.getProductionLineCode());
                // 查询上一个小时的结果
                List<ApsResult> lastList = map.get(index - 1);
                ApsResult lastResult = lastList.get(lastList.size() - 1);
                if (lastResult.getMaterialCode().equals(apsResult.getMaterialCode())) {
                    // 不换料换订单
                    this.apsNoTurnYesComplete(lastResult, apsResult, index, result);
                    // 未完成，订单可生产数减去产量
                    this.subtractOrder(apsResult, 1);
                } else {
                    // 换料
                    this.apsRecount(lastResult.getProductionLineCode(), lastResult, null, 1, index, result);
                }
            });
        }
    }

    private void apsRecount(String productionLineCode, ApsResult turnLastResult, ApsResult turnResult, int turnHours, int index, Map<String, Map<Integer, List<ApsResult>>> result) {
        Long hours = DateUtil.between(turnLastResult.getStartTime(), turnLastResult.getEndTime(), DateUnit.HOUR) + 1;
        int count = hours.intValue();
        int allCount = count + turnHours;
        // 先将结果退还订单
        this.subtractOrder(turnLastResult, -count);
        // 获取整个阶段的最优解
        ApsResult apsResult = this.maxScore2(productionLineCode, allCount);
        assert null != apsResult;
        if (apsResult.getOrderCode().equals(turnLastResult.getOrderCode())) {
            // 原解仍然是最优解
            // 再次扣减订单订单
            this.subtractOrder(turnLastResult, count);
            if (null == turnResult) {
                // 添加新的换料结果
                this.apsYesTurn(turnLastResult, index, result);
            } else {
                // 换料结果时间+1
                this.apsNoTurnNoComplete(turnResult, index, result);
            }
        } else {
            // 出现了新的最优解
            Map<Integer, List<ApsResult>> mapTime = result.get(productionLineCode);
            List<ApsResult> newList = new ArrayList<>(mapTime.get(index - allCount));
            apsResult.setStartTime(turnLastResult.getStartTime());
            apsResult.setEndTime(DateUtils.addHours(turnLastResult.getStartTime(), allCount));
            newList.add(apsResult);
            mapTime.put(index, newList);
            // 订单可生产数减去产量
            this.subtractOrder(apsResult, allCount);
        }
    }

    private void apsNoTurnNoComplete(ApsResult lastResult, int index, Map<String, Map<Integer, List<ApsResult>>> result) {
        Map<Integer, List<ApsResult>> mapTime = result.get(lastResult.getProductionLineCode());
        List<ApsResult> listProductionLine = new ArrayList<>(mapTime.get(index - 1));
        ApsResult apsResult = listProductionLine.get(listProductionLine.size() - 1);
        apsResult.setEndTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        mapTime.put(index, listProductionLine);
    }

    private void apsNoTurnYesComplete(ApsResult lastResult, ApsResult apsResult, int index, Map<String, Map<Integer, List<ApsResult>>> result) {
        apsResult.setStartTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        apsResult.setEndTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        Map<Integer, List<ApsResult>> mapTime = result.get(lastResult.getProductionLineCode());
        List<ApsResult> listProductionLine = new ArrayList<>(mapTime.get(index - 1));
        listProductionLine.add(apsResult);
        mapTime.put(index, listProductionLine);
    }

    private void apsYesTurn(ApsResult lastResult, int index, Map<String, Map<Integer, List<ApsResult>>> result) {
        ApsResult apsResult = new ApsResult();
        apsResult.setProductionLineCode(lastResult.getProductionLineCode());
        apsResult.setStartTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        apsResult.setEndTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        apsResult.setTurnFlag(true);
        Map<Integer, List<ApsResult>> mapTime = result.get(lastResult.getProductionLineCode());
        List<ApsResult> listProductionLine = new ArrayList<>(mapTime.get(index - 1));
        listProductionLine.add(apsResult);
        mapTime.put(index, listProductionLine);
    }

    private void apsNewTurn(ApsResult lastResult, ApsResult newResult, int index, Map<String, Map<Integer, List<ApsResult>>> result) {
        newResult.setStartTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        newResult.setEndTime(DateUtils.addHours(lastResult.getEndTime(), 1));
        Map<Integer, List<ApsResult>> mapTime = result.get(newResult.getProductionLineCode());
        List<ApsResult> listProductionLine = new ArrayList<>(mapTime.get(index - 1));
        listProductionLine.add(newResult);
        mapTime.put(index, listProductionLine);
    }

    /**
     * 验证生产完成
     * false 完成  true 未完成
     *
     * @param order
     * @return
     */
    private boolean checkComplete(AlgOrder order) {
        if (order != null) {
            return order.getApsQty().compareTo(BigDecimal.ZERO) > 0;
        }
        return false;
    }

    private List<ApsResult> maxScore1(List<ProductionLine> productionLineList) {
        List<ApsResult> result = new ArrayList<>();
        productionLineList.forEach(e -> {
            ApsResult apsResult = this.maxScore2(e.getCode(), 1);
            if (null != apsResult) {
                result.add(apsResult);
            }
        });
        return result;
    }

    private ApsResult maxScore2(String productionLineCode, int hours) {
        AlgOrder order = this.getMaxScoreOrder(productionLineCode, hours);
        if (null != order) {
            ApsResult apsResult = new ApsResult();
            apsResult.setProductionLineCode(productionLineCode);
            apsResult.setOrderCode(order.getCode());
            apsResult.setMaterialCode(order.getMaterialCode());
            apsResult.setTurnFlag(false);
            return apsResult;
        }
        return null;
    }

    private AlgOrder getMaxScoreOrder(String productionLineCode, int hour) {
        AlgOrder maxOrder = null;
        BigDecimal maxScore = BigDecimal.ZERO;
        List<AlgProductionLineMaterial> configList = productionLineConfigMap.get(productionLineCode);
        for (AlgOrder order : orderList) {
            if (order.getApsQty().compareTo(BigDecimal.ZERO) == 0) {
                continue;
            }
            for (AlgProductionLineMaterial config : configList) {
                if (config.getMaterialCode().equals(order.getMaterialCode())) {
                    // 可生产数量
                    BigDecimal canQty = order.getApsQty();
                    // 按时间生产数量
                    BigDecimal capacity = new BigDecimal(config.getCapacity()).multiply(new BigDecimal(hour));
                    // 参与评分的数量等于可生产数量和按时间生产数量的较小值
                    BigDecimal num = capacity.compareTo(canQty) > 0 ? canQty : capacity;
                    BigDecimal score = num.multiply(order.getRate());
                    if (score.compareTo(maxScore) > 0) {
                        maxScore = score;
                        maxOrder = order;
                    } else if (score.compareTo(maxScore) == 0) {
                        if (order.getApsQty().compareTo(maxOrder.getApsQty()) > 0) {
                            maxOrder = order;
                        }
                    }
                }
            }
        }
        return maxOrder;
    }

    private void subtractOrder(ApsResult apsResult, int hours) {
        AlgOrder order = orderList.stream().filter(e -> e.getCode().equals(apsResult.getOrderCode())).findFirst().orElse(null);
        List<AlgProductionLineMaterial> configList = productionLineConfigMap.get(apsResult.getProductionLineCode());
        AlgProductionLineMaterial config = configList.stream().filter(e -> e.getMaterialCode().equals(order.getMaterialCode())).findFirst().orElse(null);
        BigDecimal num = new BigDecimal(config.getCapacity()).multiply(new BigDecimal(hours));
        order.setApsQty(order.getApsQty().subtract(num));
    }
}
